xtask\tasks\fuzz/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use self::cargo_fuzz::CargoFuzzCommand;
5use self::parse_fuzz_crate_toml::RepoFuzzTarget;
6use crate::Xtask;
7use anyhow::Context;
8use clap::Parser;
9use serde::Deserialize;
10use std::collections::BTreeMap;
11use std::io::Write;
12use std::path::PathBuf;
13
14mod cargo_fuzz;
15mod html_coverage;
16mod init_from_template;
17mod onefuzz_schema;
18mod parse_fuzz_crate_toml;
19
20/// Xtask to interact with with fuzzing infrastructure in the HvLite repo.
21#[derive(Parser)]
22#[clap(
23    about = "Superset of `cargo fuzz` features, tailored to the HvLite repo",
24    disable_help_subcommand = true
25)]
26#[clap(after_help = r#"ADDITIONAL NOTES:
27
28    Fuzzers in the HvLite repo are required to include a
29    [package.metadata.xtask.fuzz.onefuzz-allowlist] section in their Cargo.toml.
30
31    Allowlists are used by OneFuzz to limit which files are considered when
32    generating code coverage reports.
33
34    A typical declaration might something like:
35
36        [package.metadata.xtask.fuzz.onefuzz-allowlist]
37        fuzz_my_crate = [
38            "fuzz_my_crate.rs"
39            "../src/**/*",
40            "!../src/vendored/**/*"
41        ]
42
43    In this example, code coverage reports would consider the fuzzer itself
44    and all files under the crate's `src/` directory, excluding code under
45    `src/vendored/`.
46
47    NOTE: Omitting this table will result in verification failures!
48"#)]
49pub struct Fuzz {
50    /// Subcommand
51    #[clap(subcommand)]
52    cmd: FuzzCommand,
53}
54
55#[derive(clap::Subcommand)]
56enum FuzzCommand {
57    /// Onboard a new fuzz target corresponding to a particular crate
58    Init {
59        /// Crate name to spin up fuzzing infrastructure for
60        package: String,
61
62        /// Fuzzer template to init with
63        template: init_from_template::Template,
64    },
65    /// List all available fuzz targets in the repo
66    List {
67        /// List all available fuzz *crates* in the repo.
68        ///
69        /// A fuzz crate can contain multiple fuzz targets.
70        ///
71        /// This option is mainly useful when running in CI, as it can be used
72        /// to generate an "exclude list" of fuzz crates, which we skip building
73        /// on platforms that `libfuzzer-sys` doesn't support (notably: musl).
74        #[clap(long)]
75        crates: bool,
76    },
77    /// Verify the `Cargo.toml` and directory structures of in-tree fuzzing
78    /// crates.
79    Verify,
80    /// Build fuzz targets.
81    Build {
82        /// Fuzzing targets to build. If not specified, builds all available
83        /// targets.
84        targets: Vec<String>,
85
86        /// The Rust toolchain to use. Defaults to the environment's default toolchain.
87        #[clap(long)]
88        toolchain: Option<String>,
89
90        /// Extra args to forward to each `cargo fuzz build` invocation.
91        #[clap(raw(true))]
92        extra: Vec<String>,
93    },
94    /// Run fuzz targets.
95    Run {
96        /// Fuzzing target to run.
97        target: String,
98
99        /// Path to specific repro case
100        artifact: Option<PathBuf>,
101
102        /// The Rust toolchain to use. Defaults to the environment's default toolchain.
103        #[clap(long)]
104        toolchain: Option<String>,
105
106        /// Extra args to forward to `cargo fuzz run`.
107        #[clap(raw(true))]
108        extra: Vec<String>,
109    },
110    /// Clean local fuzzing artifacts/corpus/coverage
111    Clean {
112        /// Fuzzing targets to clean. If not specified, cleans all available
113        /// targets.
114        targets: Vec<String>,
115
116        /// Don't delete corpus directories
117        #[clap(long)]
118        keep_corpus: bool,
119
120        /// Don't delete artifact directories
121        #[clap(long)]
122        keep_artifacts: bool,
123
124        /// Don't delete coverage directories
125        #[clap(long)]
126        keep_coverage: bool,
127    },
128    /// Print the `std::fmt::Debug` output for an input.
129    Fmt {
130        /// Fuzzing target the input corresponds to.
131        target: String,
132
133        /// Path to input file.
134        input: PathBuf,
135
136        /// The Rust toolchain to use. Defaults to the environment's default toolchain.
137        #[clap(long)]
138        toolchain: Option<String>,
139
140        /// Extra args to forward to `cargo fuzz fmt`.
141        #[clap(raw(true))]
142        extra: Vec<String>,
143    },
144    /// Minify a corpus.
145    Cmin {
146        /// Fuzzing target to minify the corpus of.
147        target: String,
148
149        /// The Rust toolchain to use. Defaults to the environment's default toolchain.
150        #[clap(long)]
151        toolchain: Option<String>,
152
153        /// Extra args to forward to `cargo fuzz cmin`.
154        #[clap(raw(true))]
155        extra: Vec<String>,
156    },
157    /// Minify a test case.
158    Tmin {
159        /// Fuzzing target the test case corresponds to.
160        target: String,
161
162        /// Path to test case file.
163        test_case: PathBuf,
164
165        /// The Rust toolchain to use. Defaults to the environment's default toolchain.
166        #[clap(long)]
167        toolchain: Option<String>,
168
169        /// Extra args to forward to `cargo fuzz tmin`.
170        #[clap(raw(true))]
171        extra: Vec<String>,
172    },
173    /// Run program on the generated corpus and generate coverage information.
174    Coverage {
175        /// Fuzzing target the corpus corresponds to.
176        target: String,
177
178        /// Also generate an HTML coverage report using `lcov` and `genhtml`.
179        #[clap(long)]
180        with_html_report: bool,
181
182        /// Skip rebuilding + collecting new coverage data.
183        #[clap(long, requires = "with_html_report")]
184        only_report: bool,
185
186        /// The Rust toolchain to use. Defaults to the environment's default toolchain.
187        #[clap(long)]
188        toolchain: Option<String>,
189
190        /// Extra args to forward to `cargo fuzz coverage`.
191        #[clap(raw(true))]
192        extra: Vec<String>,
193    },
194    /// Build fuzzers and construct a Onefuzz-ready drop folder.
195    Onefuzz {
196        /// Path to the OneFuzz configuration file.
197        config_path: PathBuf,
198
199        /// Output directory to emit files to.
200        out_dir: PathBuf,
201
202        /// Specific targets to include. If left blank, includes all available
203        /// targets.
204        target: Vec<String>,
205
206        /// The Rust toolchain to use. Defaults to the environment's default toolchain.
207        #[clap(long)]
208        toolchain: Option<String>,
209    },
210    /// (debug) Dump raw debug info about all available fuzz targets
211    Dump,
212}
213
214mod cargo_package_metadata {
215    use serde::Deserialize;
216    use serde::Serialize;
217    use std::collections::BTreeMap;
218
219    #[derive(Serialize, Deserialize)]
220    pub struct PackageMetadata {
221        #[serde(rename = "cargo-fuzz")]
222        pub cargo_fuzz: Option<bool>, // piggyback off cargo-fuzz infra
223        pub xtask: Option<Xtask>,
224    }
225
226    #[derive(Serialize, Deserialize)]
227    pub struct Xtask {
228        pub fuzz: Option<Fuzz>,
229    }
230
231    #[derive(Serialize, Deserialize)]
232    pub struct Fuzz {
233        #[serde(rename = "onefuzz-allowlist")]
234        pub allowlist: BTreeMap<String, Vec<String>>,
235
236        #[serde(default, rename = "target-options")]
237        pub target_options: BTreeMap<String, Vec<String>>,
238    }
239}
240
241impl Xtask for Fuzz {
242    fn run(self, ctx: crate::XtaskCtx) -> anyhow::Result<()> {
243        let fuzz_crates = parse_fuzz_crate_toml::get_repo_fuzz_crates(&ctx)?;
244        let fuzz_targets = parse_fuzz_crate_toml::get_repo_fuzz_targets(&fuzz_crates)?;
245
246        match self.cmd {
247            FuzzCommand::Init { package, template } => {
248                init_from_template::init_from_template(&ctx, package, template)?;
249            }
250            FuzzCommand::Dump => {
251                println!("{:#?}", fuzz_targets)
252            }
253            FuzzCommand::Verify => {
254                // essentially a no-op, since if we made it this far, that means
255                // that everything validated correctly.
256                log::info!("fuzzing crates were successfully verified!")
257            }
258            FuzzCommand::List { crates } => {
259                if crates {
260                    for parse_fuzz_crate_toml::FuzzCrateMetadata { crate_name, .. } in fuzz_crates {
261                        println!("{}", crate_name)
262                    }
263                } else {
264                    for (name, _meta) in fuzz_targets {
265                        println!("{}", name)
266                    }
267                }
268            }
269            FuzzCommand::Build {
270                targets,
271                toolchain,
272                extra,
273            } => {
274                let selected_fuzz_targets = filter_fuzz_targets(fuzz_targets, targets)?;
275
276                for (name, meta) in selected_fuzz_targets {
277                    println!("building '{}'", name);
278                    CargoFuzzCommand::Build.invoke(
279                        &name,
280                        &meta.fuzz_dir,
281                        &meta.target_options,
282                        toolchain.as_deref(),
283                        &extra,
284                    )?;
285                }
286            }
287            FuzzCommand::Run {
288                target: target_name,
289                artifact,
290                toolchain,
291                extra,
292            } => {
293                let target = select_fuzz_target(fuzz_targets, &target_name)?;
294                let res = CargoFuzzCommand::Run { artifact }.invoke(
295                    &target_name,
296                    &target.fuzz_dir,
297                    &target.target_options,
298                    toolchain.as_deref(),
299                    &extra,
300                );
301
302                if let Err(e) = res {
303                    log::warn!(
304                        "Reminder: Make sure you swap `cargo fuzz` with `cargo xtask fuzz` when repro-ing / minimizing failures in the HvLite repo!"
305                    );
306                    return Err(e);
307                }
308            }
309            FuzzCommand::Fmt {
310                target: target_name,
311                input,
312                extra,
313                toolchain,
314            } => {
315                let target = select_fuzz_target(fuzz_targets, &target_name)?;
316
317                CargoFuzzCommand::Fmt { input }.invoke(
318                    &target_name,
319                    &target.fuzz_dir,
320                    &target.target_options,
321                    toolchain.as_deref(),
322                    &extra,
323                )?;
324            }
325            FuzzCommand::Cmin {
326                target: target_name,
327                extra,
328                toolchain,
329            } => {
330                let target = select_fuzz_target(fuzz_targets, &target_name)?;
331
332                CargoFuzzCommand::Cmin.invoke(
333                    &target_name,
334                    &target.fuzz_dir,
335                    &target.target_options,
336                    toolchain.as_deref(),
337                    &extra,
338                )?;
339            }
340            FuzzCommand::Tmin {
341                target: target_name,
342                test_case,
343                toolchain,
344                extra,
345            } => {
346                let target = select_fuzz_target(fuzz_targets, &target_name)?;
347                let res = CargoFuzzCommand::Tmin { test_case }.invoke(
348                    &target_name,
349                    &target.fuzz_dir,
350                    &target.target_options,
351                    toolchain.as_deref(),
352                    &extra,
353                );
354
355                if let Err(e) = res {
356                    log::warn!(
357                        "Reminder: Make sure you swap `cargo fuzz` with `cargo xtask fuzz` when repro-ing / minimizing failures in the HvLite repo!"
358                    );
359                    return Err(e);
360                }
361            }
362            FuzzCommand::Coverage {
363                target: target_name,
364                with_html_report,
365                only_report,
366                toolchain,
367                extra,
368            } => {
369                let target = select_fuzz_target(fuzz_targets, &target_name)?;
370
371                if !only_report {
372                    CargoFuzzCommand::Coverage.invoke(
373                        &target_name,
374                        &target.fuzz_dir,
375                        &target.target_options,
376                        toolchain.as_deref(),
377                        &extra,
378                    )?;
379                }
380
381                if with_html_report {
382                    html_coverage::generate_html_coverage_report(
383                        &ctx,
384                        &target.fuzz_dir,
385                        &target_name,
386                    )?;
387                }
388            }
389            FuzzCommand::Onefuzz {
390                target,
391                config_path,
392                toolchain,
393                out_dir,
394            } => {
395                let selected_fuzz_targets = filter_fuzz_targets(fuzz_targets, target)?;
396
397                if !out_dir.exists() {
398                    fs_err::create_dir_all(&out_dir)?;
399                }
400
401                let config_contents = fs_err::read_to_string(config_path)
402                    .context("failed to read configuration toml")?;
403                let cfg = toml_edit::de::from_str(&config_contents)
404                    .context("failed to parse onefuzz.toml")?;
405
406                for (name, target) in &selected_fuzz_targets {
407                    log::info!("building '{}'", name);
408                    CargoFuzzCommand::Build.invoke(
409                        name,
410                        &target.fuzz_dir,
411                        &target.target_options,
412                        toolchain.as_deref(),
413                        &[],
414                    )?;
415
416                    log::info!("copying '{}' to output folder", name);
417                    // Because we call Build ourselves above we guarantee that
418                    // the built binaries are here.
419                    std::fs::copy(
420                        format!("target/x86_64-unknown-linux-gnu/release/{}", name),
421                        out_dir.join(name),
422                    )?;
423
424                    log::info!("emitting onefuzz allowlist for '{name}'");
425                    let mut allowlist_file =
426                        fs_err::File::create(out_dir.join(name).with_extension("txt"))?;
427                    for path in &target.allowlist {
428                        let Ok(path) = path.strip_prefix(&ctx.root) else {
429                            // Ok to throw away `std::path::StripPrefixError`,
430                            // it doesn't contain any additional context
431                            anyhow::bail!(
432                                "allowlist for '{name}' references file(s) outside of the HvLite directory"
433                            )
434                        };
435                        // add in "*/" to appease the OneFuzz allowlist syntax
436                        writeln!(allowlist_file, "*/{}", path.display())?;
437                    }
438                }
439
440                log::info!("emitting OneFuzzConfig.json");
441                let config_file =
442                    fs_err::File::create(out_dir.join("OneFuzzConfig").with_extension("json"))?;
443                let config = onefuzz_schema::OneFuzzConfigV3 {
444                    config_version: 3,
445                    entries: selected_fuzz_targets
446                        .into_iter()
447                        .map(|(name, target)| make_onefuzz_entry(name, target.target_options, &cfg))
448                        .collect(),
449                };
450                serde_json::to_writer(config_file, &config)?;
451            }
452            FuzzCommand::Clean {
453                targets,
454                keep_corpus,
455                keep_artifacts,
456                keep_coverage,
457            } => {
458                let selected_fuzz_targets = filter_fuzz_targets(fuzz_targets, targets)?;
459
460                for (name, meta) in selected_fuzz_targets {
461                    let rm_dir = |base: &str| -> std::io::Result<()> {
462                        let dir = meta.fuzz_dir.join(base);
463                        let target_dir = dir.join(&name);
464                        if target_dir.exists() {
465                            fs_err::remove_dir_all(dir.join(&name))?;
466                        }
467                        if dir.exists() && fs_err::read_dir(&dir)?.count() == 0 {
468                            fs_err::remove_dir(dir)?;
469                        }
470
471                        Ok(())
472                    };
473
474                    if !keep_artifacts {
475                        rm_dir("artifacts")?
476                    }
477
478                    if !keep_corpus {
479                        rm_dir("corpus")?
480                    }
481
482                    if !keep_coverage {
483                        rm_dir("coverage")?
484                    }
485                }
486            }
487        }
488
489        Ok(())
490    }
491}
492
493fn make_onefuzz_entry(
494    name: String,
495    target_options: Vec<String>,
496    cfg: &OnefuzzToml,
497) -> onefuzz_schema::Entry {
498    let my_cfg = cfg.overrides.get(&name);
499    let use_cfg = OnefuzzTomlConfig {
500        owner: my_cfg
501            .and_then(|m| m.owner.clone())
502            .unwrap_or(cfg.default.owner.clone()),
503        project_name: my_cfg
504            .and_then(|m| m.project_name.clone())
505            .unwrap_or(cfg.default.project_name.clone()),
506        ado_org: my_cfg
507            .and_then(|m| m.ado_org.clone())
508            .unwrap_or(cfg.default.ado_org.clone()),
509        ado_project: my_cfg
510            .and_then(|m| m.ado_project.clone())
511            .unwrap_or(cfg.default.ado_project.clone()),
512        ado_assigned_to: my_cfg
513            .and_then(|m| m.ado_assigned_to.clone())
514            .unwrap_or(cfg.default.ado_assigned_to.clone()),
515        ado_area_path: my_cfg
516            .and_then(|m| m.ado_area_path.clone())
517            .unwrap_or(cfg.default.ado_area_path.clone()),
518        ado_iteration_path: my_cfg
519            .and_then(|m| m.ado_iteration_path.clone())
520            .unwrap_or(cfg.default.ado_iteration_path.clone()),
521        ado_tags: my_cfg
522            .and_then(|m| m.ado_tags.clone())
523            .unwrap_or(cfg.default.ado_tags.clone()),
524    };
525
526    onefuzz_schema::Entry {
527        job_notification_email: use_cfg.owner,
528        fuzzer: onefuzz_schema::Fuzzer {
529            type_field: "libfuzzer".to_owned(),
530            fuzzing_harness_executable_name: name.clone(),
531            sources_allow_list_path: format!("{}.txt", name),
532        },
533        job_dependencies: vec![name.clone()],
534        one_fuzz_jobs: vec![onefuzz_schema::OneFuzzJob {
535            project_name: use_cfg.project_name.to_owned(),
536            target_name: name.clone(),
537            target_options,
538        }],
539        ado_template: onefuzz_schema::AdoTemplate {
540            org: use_cfg.ado_org,
541            project: use_cfg.ado_project,
542            assigned_to: use_cfg.ado_assigned_to,
543            area_path: use_cfg.ado_area_path,
544            iteration_path: use_cfg.ado_iteration_path,
545            ado_fields: onefuzz_schema::AdoFields {
546                tags: use_cfg.ado_tags,
547            },
548        },
549    }
550}
551
552/// Check that all fuzz targets in the repo follow the correct formatting.
553// DEVNOTE: used by `xtask fmt`
554#[derive(Parser)]
555pub struct VerifyFuzzers;
556
557impl Xtask for VerifyFuzzers {
558    fn run(self, ctx: crate::XtaskCtx) -> anyhow::Result<()> {
559        let fuzz_crates = parse_fuzz_crate_toml::get_repo_fuzz_crates(&ctx)?;
560        let _fuzz_targets = parse_fuzz_crate_toml::get_repo_fuzz_targets(&fuzz_crates)?;
561        Ok(())
562    }
563}
564
565fn select_fuzz_target(
566    mut fuzz_targets: BTreeMap<String, RepoFuzzTarget>,
567    target_name: &str,
568) -> anyhow::Result<RepoFuzzTarget> {
569    match fuzz_targets.remove(target_name) {
570        Some(target) => Ok(target),
571        None => anyhow::bail!("invalid fuzz target '{}'", target_name),
572    }
573}
574
575fn filter_fuzz_targets(
576    mut fuzz_targets: BTreeMap<String, RepoFuzzTarget>,
577    specific_targets: Vec<String>,
578) -> anyhow::Result<BTreeMap<String, RepoFuzzTarget>> {
579    if specific_targets.is_empty() {
580        return Ok(fuzz_targets);
581    }
582    let mut targets = BTreeMap::new();
583    for target_name in specific_targets {
584        let Some(target) = fuzz_targets.remove(&target_name) else {
585            anyhow::bail!("invalid fuzz target '{}'", target_name)
586        };
587
588        targets.insert(target_name, target);
589    }
590
591    Ok(targets)
592}
593
594pub(crate) fn complete_fuzzer_targets(ctx: &crate::XtaskCtx) -> Vec<String> {
595    (|| {
596        let fuzz_crates = parse_fuzz_crate_toml::get_repo_fuzz_crates(ctx)?;
597        let fuzz_targets = parse_fuzz_crate_toml::get_repo_fuzz_targets(&fuzz_crates)?;
598        anyhow::Ok(fuzz_targets.into_keys().collect::<Vec<String>>())
599    })()
600    .unwrap_or_default()
601}
602
603#[derive(Deserialize)]
604struct OnefuzzToml {
605    default: OnefuzzTomlConfig,
606    overrides: BTreeMap<String, OnefuzzTomlOverrides>,
607}
608
609#[derive(Deserialize)]
610struct OnefuzzTomlConfig {
611    owner: String,
612    project_name: String,
613    ado_org: String,
614    ado_project: String,
615    ado_assigned_to: String,
616    ado_area_path: String,
617    ado_iteration_path: String,
618    ado_tags: String,
619}
620
621#[derive(Deserialize)]
622struct OnefuzzTomlOverrides {
623    owner: Option<String>,
624    project_name: Option<String>,
625    ado_org: Option<String>,
626    ado_project: Option<String>,
627    ado_assigned_to: Option<String>,
628    ado_area_path: Option<String>,
629    ado_iteration_path: Option<String>,
630    ado_tags: Option<String>,
631}